<?php
/**
*	This class implements the "Circuit Breaker" stability pattern for websites. The goal is to return immediately if a resource is down
*	for an extended amount of time, this will allow user connections to not pool up waiting for time outs
*@author Jim Plush jiminoc@gmail.com
*/
class CircuitBreaker
{

	/**
	* public variable that lets the class know if log messages should be printed out to the screen, you could also change the log method
	* to write to your general logfile or database
	*/
	public $logging = false;
	
	
	
	/**
	* holds the current application ID to query the status of, you should give each external web service or resource you make contact with 
	* a unique application ID so you can store the status for each one individually
	*@param string
	*/
	protected $app_id;
	

	
	
	
	
	/**
	* constructor to instantiate the circuitbreaker, needs to query the current status
	*/
	public function __construct($app_id)
	{
		$this->app_id = $app_id;
		$this->_getAppStatus();
	}
	
	/**
	* method to call to see if the circuit for this application is open or closed, if closed that means all is well and operations can resume
	* as normal. If open then too many failures have occurred and you need to fail immediatley
	*/
	public function isClosed()
	{
		if($this->status['state'] !== 'open') {
			return true;
		}
		// get the number of seconds that have past since the circuit was opened to the current system time
		$res = mysql_query("SELECT TIME_TO_SEC(TIMEDIFF(NOW(), last_open_time)) as diff FROM circuit_breaker WHERE app_id = '{$this->app_id}'");
		$row = mysql_fetch_assoc($res);
		if($row['diff'] > $this->status['timeout']) {
			$this->_log('Timeout reached, now opening up the circuit halfway');
			// we've past the timeout, open the circuit halfway to allow just one connection through
			mysql_query("UPDATE circuit_breaker SET state = 'half', failure_count=threshold-1 WHERE app_id = '{$this->app_id}'");
			return true;
		}
		return false;
	}
	
	/**
	* used to indicate that an action was successful and we can update the status of our app
	*/
	public function success()
	{
		// operation was successful so reset this app back to happy state
		mysql_query("UPDATE circuit_breaker SET state = 'closed', failure_count=0 WHERE app_id = '{$this->app_id}'");
	}
	
	/**
	* used to indicate  a call has timed out or has failed, increment the failures, if failures exceed threshold then we need to trip the circuit
	*/
	public function fail()
	{
		self::_getDb();
		if(((int)$this->status['failure_count'] > (int)$this->status['threshold']) || $this->status['state'] === 'half' ) {
			$this->_log('Too many failures! Now opening circuit to stop further calls');
			mysql_query("UPDATE circuit_breaker SET state = 'open', last_open_time = NOW() WHERE app_id = '{$this->app_id}'");
			$this->_notify();	// send email to administrator informing them of the outage
		} else {
			mysql_query("UPDATE circuit_breaker SET failure_count = failure_count+1 WHERE app_id = '{$this->app_id}'");
		}
	}
	
	
	/**
	*	this should probably also be replaced by your general mail handler but it's here to keep things simple. Just sends an email to the admin
	*	letting them know the service is down, you'll obviously want to change the email address below
	*/
	protected function _notify()
	{
		mail('admin@site.com', "CIRCUIT BREAKER TRIPPED ON {$_SERVER['SERVER_NAME']}", "Application {$this->app_id} has failed to connect"
		." over the recommended threshold, all communication is stopped with this app");
	}

	/**
	*	you should replace the contents of this method with your own database abstraction layer or configuration DB settings. 
	*	This is only for demonstration purposes only
	*	It would also be advisable to actually use memcached over a DB for less overhead in maintaining status than a database
	*	I'm using a database here since that's what most people will have available.
	*/
	protected function _getAppStatus()
	{
		self::_getDb();	// connect to our test database
		
		$app_id = mysql_real_escape_string($this->app_id);
		if(!$result = mysql_query("SELECT * FROM circuit_breaker WHERE app_id = '{$app_id}'")) {
			throw new Exception("CB Error: Querying for the application status failed");
		}
		if(!$this->status = mysql_fetch_assoc($result)) {
			throw new Exception("No Application Found with an ID of {$app_id}");
		}
	}
	
	
	/**
	* replace this with a DB adapter class or however else you're getting your database connections
	*/
	static private function _getDb()
	{
		$conn = mysql_connect('localhost', 'root', 'root');
		mysql_select_db('demo', $conn);
	}


	/**
	* if logging is enabled then messages will be shown on the screen
	*/
	private function _log($msg)
	{
		if($this->logging) {
			echo "{$msg}<br/>";
		}
	}

}
?>